package com.googlecode.tidej.shared.jsdoc;

import java.util.ArrayList;

import com.googlecode.tidej.client.Console;
import com.googlecode.tidej.shared.model.Library;
import com.googlecode.tidej.shared.model.Model;
import com.googlecode.tidej.shared.model.Operation;
import com.googlecode.tidej.shared.model.Parameter;
import com.googlecode.tidej.shared.model.Property;
import com.googlecode.tidej.shared.model.Type;
import com.googlecode.tidej.shared.util.Tokenizer;


public class JsDocParser {

  static final JsDoc EMPTY_JSDOC = new JsDoc("");
  
  Tokenizer tokenizer;
  StringBuilder main = new StringBuilder();
  Model model;
  Library project;
  int nesting;
  boolean statementStart = true;
  String code;
  int mark;
  
  public JsDocParser(Model model, Library project, String code) {
    this.code = code;
    this.model = model;
    this.project = project;
    tokenizer = new Tokenizer(code);
    tokenizer.setReportWhitespace(true);
    tokenizer.setReportComments(true);
  }

  private static String reformat(String body, int outdent) {
    String[] lines = body.split("\n");
    int start = 0;
    while (start < lines.length && lines[start].trim().length() == 0) {
      start++;
    }
    int end = lines.length - 1;
    while (end >= 0 && lines[end].trim().length() == 0) {
      end--;
    }
    String removePrefix = outdent == 0 ? null : "          ".substring(0, outdent);
    StringBuilder sb = new StringBuilder();
    for (int i = start; i <= end; i++) {
      String s = lines[i];
      if (removePrefix != null && s.startsWith(removePrefix)) {
        sb.append(s.substring(removePrefix.length()));
      } else {
        sb.append(s);
      }
      sb.append('\n');
    }
    return sb.toString();
  }
  
  
  public void run() {
    loop:
    while (true) {
      tokenizer.setReportComments(true);
      mark = tokenizer.getPos();
      tokenizer.nextToken();
      // Switching on whitespace after nextToken so WS between recognized
      // parts does not accumulate.
      tokenizer.setReportWhitespace(true);
      switch (tokenizer.ttype) {
        case Tokenizer.TT_EOF:
          break loop;
        case Tokenizer.TT_COMMENT:
          if (!statementStart || !tokenizer.sval.startsWith("*") || !processJsDoc(null)) {
            addRaw();
          }
          break;
        case ' ':
          if (nesting == 0 && tokenizer.sval.indexOf('\n') != -1) {
            statementStart = true;
          }
          addRaw();
          break;
        case '{':
          nesting++;
          statementStart = false;
          addRaw();
          break;
        case '}':
          nesting--;
          if (nesting == 0) {
            statementStart = true;
          }
          addRaw();
          break;
        case Tokenizer.TT_WORD:
          if (!statementStart || !processStatement(EMPTY_JSDOC, null)) {
            statementStart = false;
            addRaw();
          }
          break;
        case ';':
          if (nesting == 0) {
            statementStart = true;
          }
          
        default:
          addRaw();
      }
    }
    int cut = 0;
    while (cut < main.length() && main.charAt(cut) < ' ') {
      if (main.charAt(cut) == '\n') {
        main.delete(0, cut + 1);
        cut = 0;
      } else {
        cut++;
      }
    }
  
    Operation mainOp = new Operation("(main)", null);
    mainOp.setBody(reformat(main.toString(), 0));
    project.getGlobals().setConstructor(mainOp);
  }
  
  private boolean processStatement(JsDoc jsdoc, Type owner) {
    if (owner != null) {
      Console.log("*******************************");
      Console.log("Processing property of " + owner);
    }
    tokenizer.setReportWhitespace(false);
    tokenizer.setReportComments(false);
    if (tokenizer.sval.equals("function")) {
      return processFunction(jsdoc);
    }
    ArrayList<String> path = new ArrayList<String>();
    path.add(tokenizer.sval);
    while (tokenizer.nextToken() == '.') {
      if (tokenizer.nextToken() != Tokenizer.TT_WORD) {
        Console.log("Unexpected token after dot: " + tokenizer.getPositionDescription() + "; path: " + path.toString());
        return false;
      }
      path.add(tokenizer.sval);
    }
    if (path.size() > 3) {
      Console.log("Path too long: " + path.toString());
      return false;
    }
    if (tokenizer.ttype != '=') {
      return false;
    }
    int pos = tokenizer.getPos();
    tokenizer.nextToken();
    boolean isFunction = tokenizer.sval.equals("function");
    if (path.size() < 1) {
      return false;
    }
    if (jsdoc == EMPTY_JSDOC) {
      if (!isFunction || path.get(0).equals("window") || path.get(0).equals("document")) {
        return false;
      }
    }
    int outdent;
    if (owner != null) {
      outdent = 4;
      switch(path.size()) {
      case 1: 
        break;
      case 2:
        if (!"this".equals(path.get(0))) {
          return false;
        }
        break;
      default:
        return false;
      }
    } else {
      outdent = 2;
      switch(path.size()) {
      case 1:
        if (jsdoc.constructor) {
          owner = forceLocalType(path.get(0));
        } else {
          owner = project.getGlobals();
        }
        break;
      case 2:
        owner = forceLocalType(path.get(0)).getMetaType();
        break;
      case 3:
        if (!"prototype".equals(path.get(1))) {
          return false;
        }
        owner = forceLocalType(path.get(0));
        break;
      default:
        return false;
      }
    }
   String name = path.get(path.size() - 1);
    
    return isFunction 
        ? processFunction(owner, name, jsdoc, outdent)
        : processVariable(owner, name, jsdoc, pos);
  }
  
  private Type forceLocalType(String name) {
    Type type = project.getType(name);
    if (type == null) {
      type = new Type(name);
      project.addType(type);
    }
    return type;
  }
  
  private Type parseJsDocType(String name) {
    if (name == null) {
      name = "any";
    } else {
      name = name.trim();
    }
    if (name.startsWith("{") && name.endsWith("}")) {
      name = name.substring(1, name.length() - 1).trim();
    }
    if (name.equals("*")) {
      name = "any";
    }
    Type type = model.getType(name);
    return type == null ? forceLocalType(name) : type;
  }
  
  private boolean processFunction(JsDoc jsdoc) {
    if (tokenizer.nextToken() != Tokenizer.TT_WORD) {
      return false;
    }
    if (jsdoc.constructor) {
      return processFunction(forceLocalType(tokenizer.sval), tokenizer.sval, jsdoc, 2);
    }
    return processFunction(project.getGlobals(), tokenizer.sval, jsdoc, 2);
  }
  
  private boolean processVariable(Type owner, String name, JsDoc jsdoc, int valueStart) {
    boolean result = true;
    int valueEnd = valueStart;
    int nesting = 0;
    loop:
    while (true) {
      switch(tokenizer.ttype) {
      case Tokenizer.TT_EOF:
        return false;
      case '{':
        nesting++;
        break;
      case '}':
        nesting--;
        if (nesting < 0) {
          return false;
        }
        break;
      case ';':
        if (nesting == 0) {
          break loop;
        }
        break;
      case Tokenizer.TT_WORD:
        if (!"null".equals(tokenizer.sval) && !"undefined".equals(tokenizer.sval) &&
           !"Array".equals(tokenizer.sval) && !"new".equals(tokenizer.sval)) {
          result = false;
          break loop;
        }
        break;
      }
      valueEnd = tokenizer.getPos();
      tokenizer.nextToken();
    }
    String initialValue = null;
    if (result) {
      initialValue = code.substring(valueStart, valueEnd).trim();
      if (initialValue.equals("undefined")) {
        initialValue = null;
      }
      mark = tokenizer.getPos();
    }
    Type type = (jsdoc.type != null) ?
        parseJsDocType(jsdoc.type[1]) : model.getType("any");
        
    Property property = new Property(name, type, false, initialValue);
    property.setDocumentation(jsdoc.documentation);
    owner.addProperty(property);
    return result;
  }
  
  private boolean processFunction(Type owner, String name, JsDoc jsdoc, int outdent) {
    if (tokenizer.nextToken() != '(') {
      return false;
    }
    ArrayList<String> params = new ArrayList<String>();
    tokenizer.nextToken();
    while (tokenizer.ttype != ')') {
      if (tokenizer.ttype != Tokenizer.TT_WORD) {
        return false;
      }
      params.add(tokenizer.sval);
      tokenizer.nextToken();
      if (tokenizer.ttype == ',') {
        tokenizer.nextToken();
        if (tokenizer.ttype == ')') {
          return false;
        }
      } else if (tokenizer.ttype != ')') {
        return false;
      }
    }
    tokenizer.nextToken();
    if (tokenizer.ttype != '{') {
      return false;
    }
    // 
    // Cannot return false after this point because of potential 
    // recursion, moving mark
    //
    StringBuilder body = new StringBuilder();
    int nesting = 1;
    loop:
    while (true) {
      tokenizer.setReportWhitespace(true);
      tokenizer.setReportComments(true);
      switch(tokenizer.nextToken()) {
      case Tokenizer.TT_EOF:
        return false;
      case '{':
        nesting++;
        break;
      case '}':
        nesting--;
        if (nesting == 0) {
          break loop;
        }
        break;
      case Tokenizer.TT_COMMENT:
        Console.log("tt_comment in function");
        if (jsdoc.constructor && tokenizer.sval.startsWith("*") && processJsDoc(owner)) {
          continue;
        }
        break;
      }
      body.append(tokenizer.getRaw());
    }
    int cut = 0;
    while (cut < body.length() && body.charAt(cut) <= ' ') {
      if (body.charAt(cut) == '\n') {
        body.delete(0,  cut + 1);
        cut = 0;
      } else {
        cut++;
      }
    }
    
    mark = tokenizer.getPos();
    
    Type returnType = (jsdoc.returns != null) ?
        parseJsDocType(jsdoc.returns[1]) : null;
    Operation operation = new Operation(name, returnType);
    if (jsdoc != EMPTY_JSDOC) {
      operation.setDocumentation(jsdoc.documentation);
    }
    for (String s: params) {
      Type type = null;
      String doc = null;
      if (jsdoc != null) {
        for (String[] tag: jsdoc.params) {
          if (tag[2].equals(s)) {
            type = parseJsDocType(tag[1]);
            doc = tag[3];
          }
        }
      }
      Parameter p = new Parameter(s, type, 0);
      p.setDocumentation(doc);
      operation.addParameter(p);
    }
    operation.setBody(reformat(body.toString(), outdent));
    
    if (jsdoc.constructor) {
      owner.setConstructor(operation);
    } else {
      owner.addOperation(operation);
    }
    
    return true;
  }
  
  private void addRaw() {
    main.append(code.substring(mark, tokenizer.getPos()));
  }

  private boolean processJsDoc(Type owner) {
    JsDoc jsdoc = new JsDoc(tokenizer.sval.substring(1));
    tokenizer.setReportWhitespace(false);
    tokenizer.setReportComments(false);
    tokenizer.nextToken();
    return processStatement(jsdoc, owner);
  }
  
}
